布局设置+类微信聊天时间格式化逻辑
概述
本节重点实现私信详情页的消息时间显示逻辑与页面布局方案。页面布局分为两种核心场景:头部固定与头部不固定,通过 Flex 布局实现消息区域的滚动与输入框的固定定位。同时实现类微信聊天的时间格式化显示规则。
页面布局架构
Flex 布局实现消息列表
私信详情页采用上下结构的 Flex 布局:消息列表区域自适应撑满剩余空间,输入框固定在底部。
<template>
<div class="message-detail">
<!-- 消息列表区域:flex: 1 自适应高度 -->
<div class="message-list-wrapper">
<div class="message-list">
<div
v-for="msg in messages"
:key="msg.id"
class="message-item"
>
<span class="message-time">{{ formatChatTime(msg.createdAt) }}</span>
<span class="message-content">{{ msg.content }}</span>
</div>
</div>
</div>
<!-- 输入框区域:固定底部 -->
<div class="message-input-wrapper">
<el-input
v-model="inputText"
placeholder="输入消息..."
@keyup.enter="handleSend"
/>
</div>
</div>
</template>
vue
关键样式实现
.message-detail {
display: flex;
flex-direction: column;
height: 100%;
}
/* 消息列表:flex: 1 撑满剩余空间 + 滚动 */
.message-list-wrapper {
flex: 1;
overflow-y: auto;
}
/* 输入框:固定底部,不被挤压 */
.message-input-wrapper {
flex-shrink: 0;
padding: 12px;
border-top: 1px solid var(--el-border-color);
}
css
头部固定 vs 头部不固定对比
| 特性 | 头部固定 | 头部不固定 |
|---|---|---|
| 内容区域高度 | 固定值,不随页面滚动 | 自适应,随页面滚动 |
| 消息列表滚动 | 内部出现滚动条 | 页面级滚动 |
| 输入框定位 | 固定在视口底部 | 跟随内容流 |
| 适用场景 | 桌面端、侧边栏聊天 | 移动端全屏聊天 |
| 实现方式 | height: 100%; flex: 1 | min-height; overflow: auto |
类微信聊天时间格式化逻辑
微信聊天时间显示遵循特定规则:同一天内 5 分钟以内的连续消息不重复显示时间,超过 5 分钟则显示时间戳。
时间格式化工具函数
// utils/timeFormat.ts
interface FormatOptions {
/** 间隔阈值(毫秒),默认 5 分钟 */
interval?: number
}
/**
* 类微信聊天时间格式化
* 规则:
* - 今天:显示 "HH:mm"
* - 昨天:显示 "昨天 HH:mm"
* - 今年:显示 "M月D日 HH:mm"
* - 非今年:显示 "YYYY年M月D日 HH:mm"
*/
export function formatChatTime(
timestamp: string | number | Date,
options: FormatOptions = {}
): string {
const { interval = 5 * 60 * 1000 } = options
const date = new Date(timestamp)
const now = new Date()
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const yesterdayStart = new Date(todayStart.getTime() - 86400000)
const yearStart = new Date(now.getFullYear(), 0, 1)
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const timeStr = `${hours}:${minutes}`
if (date >= todayStart) {
return timeStr
} else if (date >= yesterdayStart) {
return `昨天 ${timeStr}`
} else if (date >= yearStart) {
return `${date.getMonth() + 1}月${date.getDate()}日 ${timeStr}`
} else {
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日 ${timeStr}`
}
}
/**
* 判断是否需要显示时间标签
* 规则:与上一条消息间隔超过阈值则显示
*/
export function shouldShowTimeLabel(
currentTimestamp: string | number | Date,
previousTimestamp?: string | number | Date | null,
interval: number = 5 * 60 * 1000
): boolean {
if (!previousTimestamp) return true
const diff = new Date(currentTimestamp).getTime() - new Date(previousTimestamp).getTime()
return diff >= interval
}
typescript
在组件中使用时间格式化
<script setup lang="ts">
import { ref, computed } from 'vue'
import { formatChatTime, shouldShowTimeLabel } from '@/utils/timeFormat'
interface Message {
id: number
content: string
createdAt: string
senderId: number
}
const messages = ref<Message[]>([])
/** 获取带时间标签的消息列表 */
const displayMessages = computed(() => {
return messages.value.map((msg, index) => {
const prev = index > 0 ? messages.value[index - 1] : null
return {
...msg,
showTime: shouldShowTimeLabel(msg.createdAt, prev?.createdAt),
formattedTime: formatChatTime(msg.createdAt)
}
})
})
</script>
<template>
<div class="message-list">
<template v-for="msg in displayMessages" :key="msg.id">
<div v-if="msg.showTime" class="time-label">
{{ msg.formattedTime }}
</div>
<div class="message-item" :class="{ 'is-self': msg.senderId === currentUserId }">
<div class="message-content">{{ msg.content }}</div>
</div>
</template>
</div>
</template>
<style scoped>
.time-label {
text-align: center;
padding: 8px 0;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.message-item {
display: flex;
margin: 4px 12px;
}
.message-item.is-self {
justify-content: flex-end;
}
.message-content {
max-width: 70%;
padding: 8px 12px;
border-radius: 8px;
background-color: var(--el-fill-color-light);
}
.is-self .message-content {
background-color: var(--el-color-primary-light-3);
color: #fff;
}
</style>
vue
实践要点
- 消息列表使用
flex: 1撑满容器剩余空间,overflow-y: auto实现内容溢出滚动 - 输入框区域使用
flex-shrink: 0防止被压缩,始终固定在底部 - 时间格式化采用分级策略:今天/昨天/今年/非今年,匹配微信展示规则
- 时间标签通过
shouldShowTimeLabel与前一条消息对比间隔(默认 5 分钟),避免频繁显示 - 头部固定场景下,内容区域通过
height: 100%约束高度,滚动条出现在消息列表内部
↑